import math

import Box2D.b2 as b2
from pyglet.gl import *

from camera import world_to_pixels
import config
from constants import (PORTAL_LENGTH, PORTAL_FACE_LENGTH,
                       PORTAL_ARROW_LENGTH, PORTAL_ARROW_HEAD_LENGTH)
from debug import msg, SomethingIsWrongError
import entity
import resources


class Portal(entity.StaticEdge):
    def __init__(self, mode, _id, pos, init_rotation, facing_left):
        """__init__
        Arguments:
        mode -- The mode that created the portal.
        _id -- A unique identifier for the portal.
        pos -- The position of the base of the portal.
        init_rotation -- The counter-clockwise rotation of the portal.
        facing_left -- True if the portal is facing left relative
                       to the gravity.
        """
        self._id = _id
        self.gravity_dir = b2.vec2(math.sin(init_rotation), -math.cos(init_rotation))
        self.gravity_dir.Normalize()
        self.facing_left = facing_left
        self.pos = b2.vec2(pos)
        self.init_rotation = init_rotation

        self.mask_edge_bot = None
        self.mask_edge_back = None
        self.mask_edge_top = None

        self.bottom_fix = None
        self.top_fix = None

        self.activated = False

        portal_top_local = -self.gravity_dir * PORTAL_LENGTH
        portal_top = b2.vec2(pos) + portal_top_local

        super(Portal, self).__init__(
            mode, [pos, portal_top],
            e_type="portal", isSensor=True)

        self.create_dat_box()
        self.create_portalmasks()

    def create_dat_box(self):
        """
        Creates fixtures and edges around the back of portals.
        """

        for e in (self.mask_edge_bot,
                  self.mask_edge_back,
                  self.mask_edge_top):
            if e is not None:
                self.mode.world.DestroyBody(e.body)

        for f in (self.bottom_fix,
                  self.top_fix):
            if f is not None:
                self.body.DestroyFixture(f)

        # Referencing destroyed fixtures causes a segfault,
        #so we need to be _careful_!
        self.mask_fixtures_top = None
        self.mask_fixtures_back = None
        self.mask_fixtures_bot = None
        self.bottom_fix = None
        self.top_fix = None

        # To make sure a player standing on top of the portal doesn't
        # trigger begin_warp.
        margin = 0.01

        portal_top = (self.body.position
                      - (PORTAL_LENGTH + margin) * self.gravity_dir)
        portal_bottom = self.body.position + margin * self.gravity_dir

        back_top = (portal_top
                    + ({True: -1, False: 1}[self.facing_left]
                       * (-b2.vec2(-self.gravity_dir[1], self.gravity_dir[0]))
                       * PORTAL_LENGTH))
        back_bottom = (portal_bottom
                       + ({True: -1, False: 1}[self.facing_left]
                          * (-b2.vec2(-self.gravity_dir[1], self.gravity_dir[0]))
                          * PORTAL_LENGTH))
        zz_top = 1979

        self.mask_edge_top = entity.StaticEdge(
            self.mode, [portal_top, back_top],
            e_type="wall")
        self.mask_edge_back = entity.StaticEdge(
            self.mode, [back_top, back_bottom],
            e_type="wall")
        self.mask_edge_bot = entity.StaticEdge(
            self.mode, [portal_bottom, back_bottom],
            e_type="wall")

        # Some extra boxes to force players to "jump down" into
        # portals. Weird behaivour otherwise.
        B = 0.01

        self.bottom_fix = self.body.CreatePolygonFixture(
            box=(B, B, (b2.mat22(-self.body.angle)
                        * (portal_top - self.body.position)), 0),
            userData={"self" : self}, isSensor=False)
        self.top_fix = self.body.CreatePolygonFixture(
            box=(B, B, (b2.mat22(-self.body.angle)
                        * (portal_bottom - self.body.position)), 0),
            userData={"self" : self}, isSensor=False)

    def create_portalmasks(self):
        """
        Creates images to mask the backsides of portals.
        """
        # Tries to delete existing masks.
        try:
            self.mask_sprite.delete()
        except:
            pass

        if self.facing_left:
            anchor = (0, 0)
        else:
            anchor = (1, 0)

        mask_image = resources.image("data/img/portal_on.png", anchor, flip_x = not self.facing_left)

        self.mask_sprite = pyglet.sprite.Sprite(
            mask_image, x=world_to_pixels(self.body.position[0]),
            y=world_to_pixels(self.body.position[1]),
            batch=self.mode.batch_foreground)

        self.mask_sprite.rotation = - (self.init_rotation + self.body.angle) * 180 / math.pi

    def rotate(self, half_pi_rots):
        """
        Rotates the portal in right angles.

        Attributes:
        half_pi_rots -- the multiple of 90 degrees to
        rotate portal.
        """
        angle = half_pi_rots * math.pi / 2.0
        self.body.angle += angle
        self.gravity_dir = b2.mat22(angle)*self.gravity_dir
        self.create_dat_box()
        self.post_transformation_update()

    def flip(self):
        """
        Flips the portal locally left-right
        """
        self.facing_left = not self.facing_left
        self.create_dat_box()
        self.post_transformation_update()

    def post_transformation_update(self):
        """
        Updates the portal after a transformation
        """
        self.create_portalmasks()
        self.draw()

    def draw(self):
        # Set up some points for our drawing
        beginning = self.body.position - self.gravity_dir * PORTAL_LENGTH
        end = self.body.position
        arrow_end = self.body.position + self.gravity_dir * PORTAL_ARROW_LENGTH
        arrow_head_1_end = (arrow_end +
                            (-self.gravity_dir + b2.vec2(-self.gravity_dir[1],
                                                     self.gravity_dir[0]))
                            * PORTAL_ARROW_HEAD_LENGTH)
        arrow_head_2_end = (arrow_end +
                            (-self.gravity_dir - b2.vec2(-self.gravity_dir[1],
                                                     self.gravity_dir[0]))
                            * PORTAL_ARROW_HEAD_LENGTH)
        face_1_end = (beginning
                      + ({True: -1, False: 1}[self.facing_left]
                         * b2.vec2(-self.gravity_dir[1], self.gravity_dir[0])
                         * PORTAL_FACE_LENGTH))
        face_2_end = (end
                      + ({True: -1, False: 1}[self.facing_left]
                        * b2.vec2(-self.gravity_dir[1], self.gravity_dir[0])
                        * PORTAL_FACE_LENGTH))

        # Convert them into pixel coordinates
        beginning = map(world_to_pixels, beginning)
        end = map(world_to_pixels, end)
        arrow_end = map(world_to_pixels, arrow_end)
        arrow_head_1_end = map(world_to_pixels, arrow_head_1_end)
        arrow_head_2_end = map(world_to_pixels, arrow_head_2_end)
        face_1_end = map(world_to_pixels, face_1_end)
        face_2_end = map(world_to_pixels, face_2_end)

        pyglet.graphics.draw(12, GL_LINES,
                             ("v2f", (beginning[0], beginning[1],
                                      end[0], end[1],

                                      end[0], end[1],
                                      arrow_end[0], arrow_end[1],

                                      arrow_head_1_end[0], arrow_head_1_end[1],
                                      arrow_end[0], arrow_end[1],

                                      arrow_head_2_end[0], arrow_head_2_end[1],
                                      arrow_end[0], arrow_end[1],

                                      beginning[0], beginning[1],
                                      face_1_end[0], face_1_end[1],

                                      end[0], end[1],
                                      face_2_end[0], face_2_end[1])),
                             ("c3B", (9, 111, 219) * 12))

    def transform_vector(self, v, other):
        proj_length = self.gravity_dir.dot(v)

        if v.dot(b2.vec2(-self.gravity_dir[1],
                         self.gravity_dir[0])) >= 0:
            orth_length = (v -
                           proj_length * self.gravity_dir).length
        else:
            orth_length = -(v -
                            proj_length * self.gravity_dir).length

        new_proj = proj_length * other.gravity_dir

        if self.facing_left ^ other.facing_left:
            new_orth_compl = orth_length * b2.vec2(-other.gravity_dir[1],
                                                   other.gravity_dir[0])
        else:
            new_orth_compl = -orth_length * b2.vec2(-other.gravity_dir[1],
                                                    other.gravity_dir[0])

        vec = new_proj + new_orth_compl

        if vec.length == 0:
            cos_rot = 0
        else:
            cos_rot = vec.dot((0, -1))/vec.length

        if vec.dot((1,0)) < 0:
            rot = -math.acos(cos_rot)
        else:
            rot = math.acos(cos_rot)

        return {"vec": vec, "rot": rot}

    def begin_warp(self, obj):
        """Spawn a copy of the object at all portals linked to self."""

        if self in obj.exit_portals:
            return

        if obj.entrance_portal is None:
            obj.entrance_portal = self

        other_portals = list(
            self.mode.level.portal_connections.get_unordered(self._id))

        # If we are linked to the portal that the player entered through,
        # we don't want to spawn another object at the other ends.

        # if not obj.entrance_portal in other_portals:
        for portal in other_portals:

            def f(portal=portal):
                rot = self.transform_vector(obj.gravity_dir, portal)["rot"]
                pos = (portal.pos -
                       self.transform_vector(self.pos -
                                             obj.body.position, portal)["vec"])
                vel = self.transform_vector(obj.body.linearVelocity, portal)["vec"]

                o = obj.portalable.add_instance(
                    (pos, vel, rot),
                    front_is_left=(not (obj.front_is_left or (self.facing_left ^ portal.facing_left))
                                   or (obj.front_is_left and (self.facing_left ^ portal.facing_left))))

                o.body.angularVelocity = obj.body.angularVelocity

                o.entrance_portal = obj.entrance_portal
                o.exit_portals.append(portal)

            self.mode.world_poststep_cbs.append(f)

    def end_warp(self, obj):
        if not obj.entrance_portal is self:
            return

        if self.facing_left:
            normal = (-self.gravity_dir[1], self.gravity_dir[0])
        else:
            normal = (self.gravity_dir[1], -self.gravity_dir[0])

        dp = (obj.body.position - self.pos).dot(normal)

        if dp < 0:
            # The object did not pass through the portal.
            # It went back to the side it came from.
            objects = [o for o in obj.portalable.objects if self is o.entrance_portal and
                       o.exit_portals is not [] and o is not obj]#(o.body.position - self.pos).dot(normal) >= 0]

            def f(objects=objects):
                for o in objects:
                    try:
                        obj.portalable.objects.remove(o)
                    except ValueError:
                        pass

                    o.mode.world.DestroyBody(o.body)

                    try:
                        o.sprite.delete()
                    except:
                        pass

                    try:
                        o.mode.world.DestroyBody(o.pendulum_body)
                        o.pendulum_sprite.delete()
                    except:
                        pass

                    pass
                    #del o
            self.mode.world_poststep_cbs.append(f)

        elif dp > 0:
            # The object did go through the portal.
            def f():
                obj.mode.world.DestroyBody(obj.body)
                obj.sprite.delete()

                try:
                    obj.mode.world.DestroyBody(obj.pendulum_body)
                    obj.pendulum_sprite.delete()
                except:
                    pass

                obj.portalable.objects.remove(obj)

                objects = [o for o in obj.portalable.objects if self is o.entrance_portal and
                           o.exit_portals is not []]
                for o in objects:
                    o.entrance_portal = None
                    o.exit_portals = []

            self.mode.world_poststep_cbs.append(f)
        else:
            raise SomethingIsWrongError("end_warp position check is 0!"
                                     " Don't the fixtures at the ends work?")

        obj.entrance_portal = None
        try:
            obj.exit_portals.remove(self)
        except ValueError:
            pass


def draw_connections(portal_connections):
    portals = portal_connections.objects

    for portal1 in portals:
        for portal2 in portal_connections.get_unordered(portal1._id):
            beginning = map(world_to_pixels,
                            (portal1.pos
                             - portal1.gravity_dir * PORTAL_LENGTH / 2.0))
            end = map(world_to_pixels,
                      (portal2.pos
                       - portal2.gravity_dir * PORTAL_LENGTH / 2.0))

            pyglet.graphics.draw(2, GL_LINES,
                                 ("v2f", (beginning[0], beginning[1],
                                          end[0], end[1])),
                                 ("c4B", (255, 100, 0, int(255*0.2)) * 2))
